#!/usr/bin/env node

var _ = require("lodash");
var Benchmark = require("benchmark");
var exec = require("child_process").exec;
var fs = require("fs");
var path = require("path");
var mkdirp = require("mkdirp");
var async = require("../");
var suiteConfigs = require("./suites");

var args = require("yargs")
  .usage("Usage: $0 [options] [tag1] [tag2]")
  .describe("g", "run only benchmarks whose names match this regex")
  .alias("g", "grep")
  .default("g", ".*")
  .describe("i", "skip benchmarks whose names match this regex")
  .alias("i", "reject")
  .default("i", "^$")
  .describe("l", "maximum running time per test (in seconds)")
  .alias("l", "limit")
  .default("l", 2)
  .help('h')
  .alias('h', 'help')
  .example('$0 0.9.2 0.9.0', 'Compare v0.9.2 with v0.9.0')
  .example('$0 0.9.2', 'Compare v0.9.2 with the current working version')
  .example('$0', 'Compare the latest tag with the current working version')
  .example('$0 -g each', 'only run the each(), eachLimit() and  eachSeries() benchmarks')
  .example('')
  .argv;

var grep = new RegExp(args.g, "i");
var reject = new RegExp(args.i, "i");

var version0 = args._[0] || require("../package.json").version;
var version1 = args._[1] || "current";
var versionNames = [version0, version1];
var benchOptions = {defer: true, minSamples: 1, maxTime: +args.l};
var versions;
var wins = {};
var totalTime = {};
totalTime[version0] = wins[version0] = 0;
totalTime[version1] = wins[version1] = 0;

console.log("Comparing " + version0 + " with " + version1 +
  " on Node " + process.version);
console.log("--------------------------------------");


async.eachSeries(versionNames, cloneVersion, function (err) {
  if (err) { throw err; }
  versions = versionNames.map(requireVersion);

  var suites = suiteConfigs
    .map(setDefaultOptions)
    .reduce(handleMultipleArgs, [])
    .map(setName)
    .filter(matchesGrep)
    .filter(doesNotMatch)
    .map(createSuite);

  async.eachSeries(suites, runSuite, function () {
    var totalTime0 = +totalTime[version0].toPrecision(3);
    var totalTime1 = +totalTime[version1].toPrecision(3);

    var wins0 = wins[version0];
    var wins1 = wins[version1];

    if ( Math.abs((totalTime0 / totalTime1) - 1) < 0.01) {
      // if < 1% difference, we're likely within the margins of error
      console.log("Both versions are about equal " +
        "(" + totalTime0 + "ms total vs. " + totalTime1  + "ms total)");
    } else if (totalTime0 < totalTime1) {
      console.log(version0 + " faster overall " +
        "(" + totalTime0 + "ms total vs. " + totalTime1  + "ms total)");
    } else if (totalTime1 < totalTime0) {
      console.log(version1 + " faster overall " +
        "(" + totalTime1 + "ms total vs. " + totalTime0  + "ms total)");
    }

    if (wins0 > wins1) {
      console.log(version0 + " won more benchmarks " +
        "(" + wins0 + " vs. " + wins1  + ")");
    } else if (wins1 > wins0) {
      console.log(version1 + " won more benchmarks " +
        "(" + wins1 + " vs. " + wins0  + ")");
    } else {
      console.log("Both versions won the same number of benchmarks " +
        "(" + wins0 + " vs. " + wins1  + ")");
    }
  });
});

function runSuite(suite, callback) {
  suite.on("complete", function () {
    callback();
  }).run({async: true});
}

function setDefaultOptions(suiteConfig) {
  suiteConfig.args = suiteConfig.args || [[]];
  suiteConfig.setup = suiteConfig.setup || function () {};
  return suiteConfig;
}

function handleMultipleArgs(list, suiteConfig) {
  return list.concat(suiteConfig.args.map(function (args) {
    return _.defaults({args: args}, suiteConfig);
  }));
}

function setName(suiteConfig) {
  suiteConfig.name = suiteConfig.name + "(" + suiteConfig.args.join(",") + ")";
  return suiteConfig;
}

function matchesGrep(suiteConfig) {
  return !!grep.exec(suiteConfig.name);
}

function doesNotMatch(suiteConfig) {
  return !reject.exec(suiteConfig.name);
}

function createSuite(suiteConfig) {
  var suite = new Benchmark.Suite();
  var args = suiteConfig.args;
  var errored = false;

  function addBench(version, versionName) {
    var name = suiteConfig.name + " " + versionName;

    try {
      suiteConfig.setup(1);
      suiteConfig.fn(version, function () {});
    } catch (e) {
      console.error(name + " Errored");
      errored = true;
      return;
    }

    suite.add(name, function (deferred) {
      suiteConfig.fn(version, function () {
        deferred.resolve();
      });
    }, _.extend({
      versionName: versionName,
      setup: _.partial.apply(null, [suiteConfig.setup].concat(args)),
      onError: function (err) {
        console.log(err.stack);
      }
    }, benchOptions));
  }

  addBench(versions[0], versionNames[0]);
  addBench(versions[1], versionNames[1]);


  return suite.on('cycle', function(event) {
    var mean = event.target.stats.mean * 1000;
    console.log(event.target + ", " + (+mean.toPrecision(3)) + "ms per run");
    var version = event.target.options.versionName;
    if (errored) return;
    totalTime[version] += mean;
  })
  .on('error', function (err) { console.error(err); })
  .on('complete', function() {
    if (!errored) {
      var fastest = this.filter('fastest');
      if (fastest.length === 2) {
        console.log("Tie");
      } else {
        var winner = fastest[0].options.versionName;
        console.log(winner + ' is faster');
        wins[winner]++;
      }
    }
    console.log("--------------------------------------");
  });

}

function requireVersion(tag) {
  if (tag === "current") {
    return async;
  }

  return require("./versions/" + tag + "/");
}

function cloneVersion(tag, callback) {
  if (tag === "current") return callback();

  var versionDir = __dirname + "/versions/" + tag;
  mkdirp.sync(versionDir);
  fs.open(versionDir + "/package.json", "r", function (err, handle) {
    if (!err) {
      // version has already been cloned
      fs.close(handle);
      return callback();
    }

    var repoPath = path.join(__dirname, "..");

    var cmd = "git clone --branch " + tag + " " + repoPath + " " + versionDir;

    exec(cmd, function (err) {
      if (err) {
        throw err;
      }
      callback();
    });

  });
}
